/*
 * Copyright (C) 2008 GeoScheduler Team, as stated on <http://www.geoscheduler.org/authors>.
 * 
 * This file is part of GeoScheduler.
 * 
 * GeoScheduler is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * GeoScheduler is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with GeoScheduler. If not, see <http://www.gnu.org/licenses/>.
 */
package org.geoscheduler.commons.tools;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.geoscheduler.model.Location;

/**
 * Class GeoTools provides basic methods for geological computations. It provides methods for
 * conversions between different representations of degrees and computes distortions.
 * 
 * All methods are provided as public static, no instantiation is needed.
 * 
 * @author srakyi
 */
public class GeoTools {
	
	public static char HEMISPHERE_NOT_SET = 'X';
	
	public static void main(String... x) {
		System.out.println(sexagesimalTo1E6("50°4'45.961\"N"));
		System.out.println(sexagesimalTo1E6("14°25'48.819\"E"));
		
		Location loc = new Location();
		loc.setLat(50090193);
		loc.setLon(14400323);
		loc.setRadius(200);
		System.out.println(loc.toString());
		
		// default location returned by andriod
		System.out.println(E6ToSexagesimal(37429410, true));
		System.out.println(E6ToSexagesimal(-122101875, false));
		
		System.out.println(sexagesimalTo1E6("37°25'45.876\"N"));
		System.out.println(sexagesimalTo1E6("122°6'6.75\"W"));
		System.out.println(GeoTools.sexagesimalToDecimal("122°6'6.75\"W"));
	}
	
	/**
	 * Converts degrees in String (in sexagesimal representation) to decimal representation.
	 * If you'd multiply this value by 1000000 and round it, you'd get 1E6 representation.
	 * 
	 * For example 50°4'47.134"N is converted to 50.079759444130794.
	 * 
	 * @param sexagesimal	Sexagesimal representation of degrees.
	 * @return	Decimal representation of the same value.
	 */
	public static double sexagesimalToDecimal(String sexagesimal) {
		Pattern p = Pattern.compile("(-{0,1})([1-9]{0,2}[0-9])°(([1-6]{0,1}[0-9])'(([1-6]{0,1}[0-9](\\.[0-9]{0,5}){0,1})\"){0,1}){0,1}([NS]|[EW]){0,1}.*");
		Matcher m = p.matcher(sexagesimal);
		
		int sign = 1;
		short degrees = 0;
		short minutes = 0;
		float seconds = 0;
		char hemisphere = HEMISPHERE_NOT_SET;
		
		if (m.find()) {
			if ((m.group(1) != null) && (!"".equals(m.group(1)))) {
				sign = -1;
			}
			
			degrees = Short.parseShort(m.group(2));
			
			if (m.group(4) != null) {
				minutes = Short.parseShort(m.group(4));
			}
			
			if (m.group(6) != null) {
				seconds = Float.parseFloat(m.group(6));
			}
			
			if (m.group(8) != null) {
				hemisphere = m.group(8).charAt(0);
			}
		} else {
			// FIXME [srakyi] really this exception?
			throw new IllegalArgumentException("Invalid position specification (can't parse " + sexagesimal + ").");
		}
		
		double decimal = degrees + ((double)minutes / 60) + ((double)seconds / 3600);
		
		if (hemisphere != HEMISPHERE_NOT_SET) {
			if ((hemisphere == 'S') || (hemisphere == 'W')) {
				decimal = -decimal;
			}
		}
		
		return sign * decimal;
	}
	
	/**
	 * Converts degrees in String (in sexagesimal representation) to 1E6 representation. Eg. integer with seven digits.
	 * If you'd divide this number by 1000000, you'd get decimal representation.
	 * 
	 * For example 50°4'47.134"N is converted to 50079759.
	 * 
	 * @param sexagesimal	Sexagesimal representation of degrees.
	 * @return	1E6 representation of the same value.
	 */
	public static int sexagesimalTo1E6(String sexagesimal) {
		return (int)Math.round(GeoTools.sexagesimalToDecimal(sexagesimal) * 1000000);
	}
	
	/**
	 * Converts 1E6 representation of degrees to sexagesimal form.
	 * 
	 * For example 50079759 is converted to 50°4'47.132"N.
	 * 
	 * @param e6	1E6 representation to be converted.
	 * @return	String with sexagesimal representation. 
	 */
	public static String E6ToSexagesimal(int e6, boolean isLatitude) {
		double dec = (double)e6 / (double)1000000;
		
		char suffix;
		if (isLatitude) {
			suffix = 'N';
			if (dec < 0) {
				suffix = 'S';
				dec = -dec;
			}
		} else {
			suffix = 'E';
			if (dec < 0) {
				suffix = 'W';
				dec = -dec;
			}
		}
		
		StringBuffer sexa = new StringBuffer();
		
		int degrees = (int)dec;
		sexa.append(degrees).append("°");
		
		double minutesD = ((dec - degrees) * 60);
		int minutes = (int)minutesD;
		sexa.append((int)(minutes)).append("'");
		
		double secondsD = (minutesD - minutes) * 60;
		sexa.append((double)Math.round(secondsD * 1000) / 1000).append("\"");
		
		sexa.append(suffix);
		
		return sexa.toString();
	}
	
	/**
	 * Converts degrees to radians.
	 * 
	 * @param degrees	Degrees value.
	 * @return	Same value in radians.
	 */
	public static double deg2rad(double degrees) {
		return degrees * ((2.0 * Math.PI)/360.0);
	}
	
	/**
	 * Computes length (in meters) of one degree of latitude on given "latitude line".
	 * Latitude degree length are almost same on equator and poles (only difference is
	 * made by flattening of Earth).
	 * 
	 * Inspired by http://www.csgnetwork.com/degreelenllavcalc.html.
	 * 
	 * @param latitude	Latitude line we're on (90 would be north pole, 0 is equator).
	 * @return	Length of one latitude degree on given latitude line.
	 */
	public static double oneLatitudeDegreeToMeters(double latitude) {
		double lat = deg2rad(latitude);
		
		double m1 = 111132.92;	// latitude calculation term 1
		double m2 = -559.82;	// latitude calculation term 2
		double m3 = 1.175;		// latitude calculation term 3
		double m4 = -0.0023;	// latitude calculation term 4
		
		return m1 + (m2 * Math.cos(2 * lat)) + (m3 * Math.cos(4 * lat)) +	(m4 * Math.cos(6 * lat));
	}
	
	/**
	 * Computes length (in meters) of one degree of longitude on given "latitude line".
	 * There is big difference between degree length on equator and near poles.
	 * 
	 * Inspired by http://www.csgnetwork.com/degreelenllavcalc.html.
	 * 
	 * @param latitude	Latitude line we're on (90 would be north pole, 0 is equator).
	 * @return	Length of one longitude degree on given latitude line.
	 */
	public static double oneLongitudeDegreeToMeters(double latitude) {
		double lat = deg2rad(latitude);
		
		double p1 = 111412.84;	// longitude calculation term 1
		double p2 = -93.5;		// longitude calculation term 2
		double p3 = 0.118;		// longitude calculation term 3
		
		return (p1 * Math.cos(lat)) + (p2 * Math.cos(3 * lat)) + (p3 * Math.cos(5 * lat));
	}
	
	/**
	 * Returns amount of latitude degrees coresponding to given radius on given position.
	 * This differs very much for different latitudes - just imagine wireframe of globe mapped to plane.
	 *  
	 * @param lat		Latitude the radius is around.
	 * @param lon		Longitude the radius is around.
	 * @param radius	Radius in meters.
	 * @return	How many latitude degrees is given radius on given position.
	 */
	public static double getLatitudeDistanceInDegrees(int lat, int lon, int radius) {
		// how many degrees is one meter
		double meter = 1 / oneLatitudeDegreeToMeters((double)lat / 1000000);
		
		// we need radius meters
		return meter * radius;
	}
	
	/**
	 * Same as {@link GeoTools.getLatitudeDistanceInDegrees(int, int, int)}, but for longitude radius.
	 */
	public static double getLongitudeDistanceInDegrees(int lat, int lon, int rad) {
		// how many degrees is one meter
		double meter = 1 / oneLongitudeDegreeToMeters((double)lat / 1000000);
		
		// we need rad meters
		return meter * rad;
	}
}
